<!DOCTYPE html>
<link rel="help" src="https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-at-rule">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/web-animations/testcommon.js"></script>
<style>
  #scrollers > div {
    overflow: scroll;
    width: 100px;
    height: 100px;
  }
  #scrollers > div > div {
    height: 200px;
  }
  @keyframes expand {
    from { width: 100px; }
    to { width: 200px; }
  }
  #element {
    width: 0px;
    height: 20px;
    animation-name: expand;
    animation-duration: 1e10s;
    animation-timing-function: linear;
  }
</style>
<div id=scrollers>
  <div id=scroller1><div></div></div>
  <div id=scroller2><div></div></div>
</div>
<div id=container></div>
<script>
  // Force layout of scrollers.
  scroller1.offsetTop;
  scroller2.offsetTop;

  scroller1.scrollTop = 20;
  scroller2.scrollTop = 40;

  function insertElement() {
    let element = document.createElement('div');
    element.id = 'element';
    container.append(element);
    return element;
  }

  function insertSheet(text) {
    let style = document.createElement('style');
    style.textContent = text;
    container.append(style);
    return style;
  }

  // Insert an @scroll-timeline rule given 'options', where each option
  // has a reasonable default.
  function insertScrollTimeline(options) {
    if (typeof(options) == 'undefined')
      options = {};
    if (typeof(options.name) == 'undefined')
      options.name = 'timeline';
    if (typeof(options.source) == 'undefined')
      options.source = 'selector(#scroller1)';
    if (typeof(options.timeRange) == 'undefined')
      options.timeRange = '1e10s';
    if (typeof(options.start) == 'undefined')
      options.start = '0px';
    if (typeof(options.end) == 'undefined')
      options.end = '100px';
    return insertSheet(`
      @scroll-timeline ${options.name} {
        source: ${options.source};
        time-range: ${options.timeRange};
        start: ${options.start};
        end: ${options.end};
      }
    `);
  }

  // Runs a test with dynamically added/removed elements or CSS rules.
  // Each test is instantiated twice: once for the initial style resolve where
  // the DOM change was effectuated, and once after scrolling.
  function dynamic_rule_test(func, description) {
    // assert_width is an async function which verifies that the computed value
    // of 'width' is as expected.
    const instantiate = (assert_width, description) => {
      promise_test(async (t) => {
        try {
          await func(t, assert_width);
        } finally {
          while (container.firstChild)
            container.firstChild.remove();
        }
      }, description);
    };

    // Verify that the computed style is as expected immediately after the
    // rule change took place.
    instantiate(async (element, expected) => {
      assert_equals(getComputedStyle(element).width, expected);
    }, description + ' [immediate]');

    // Verify that the computed style after scrolling a bit.
    instantiate(async (element, expected) => {
      scroller1.scrollTop = scroller1.scrollTop + 1;
      scroller2.scrollTop = scroller2.scrollTop + 1;
      await waitForNextFrame();
      scroller1.scrollTop = scroller1.scrollTop - 1;
      scroller2.scrollTop = scroller2.scrollTop - 1;
      await waitForNextFrame();
      assert_equals(getComputedStyle(element).width, expected);
    }, description + ' [scroll]');
  }

  dynamic_rule_test(async (t, assert_width) => {
    let element = insertElement();

    // This element initially has a DocumentTimeline.
    await assert_width(element, '100px');

    // Switch to scroll timeline.
    let sheet1 = insertScrollTimeline();
    let sheet2 = insertSheet('#element { animation-timeline: timeline; }');
    await assert_width(element, '120px');

    // Switching from ScrollTimeline -> DocumentTimeline should preserve
    // current time.
    sheet1.remove();
    sheet2.remove();
    await assert_width(element, '120px');
  }, 'Switching between document and scroll timelines');

  dynamic_rule_test(async (t, assert_width) => {
    let element = insertElement();

    // Note: #scroller1 is at 20%, and #scroller2 is at 40%.
    insertScrollTimeline({name: 'timeline1', source: 'selector(#scroller1)'});
    insertScrollTimeline({name: 'timeline2', source: 'selector(#scroller2)'});

    insertSheet(`
      .tl1 { animation-timeline: timeline1; }
      .tl2 { animation-timeline: timeline2; }
    `);

    await assert_width(element, '100px');

    element.classList.add('tl1');
    await assert_width(element, '120px');

    element.classList.add('tl2');
    await assert_width(element, '140px');

    element.classList.remove('tl2');
    await assert_width(element, '120px');

    // Switching from ScrollTimeline -> DocumentTimeline should preserve
    // current time.
    element.classList.remove('tl1');
    await assert_width(element, '120px');

  }, 'Changing computed value of animation-timeline changes effective timeline');

  dynamic_rule_test(async (t, assert_width) => {
    let element = insertElement();

    insertScrollTimeline({source: 'selector(#scroller1)'});

    insertSheet(`
      .scroll { animation-timeline: timeline; }
      .none { animation-timeline: none; }
    `);

    // DocumentTimeline applies by default.
    await assert_width(element, '100px');

    // DocumentTimeline -> none
    element.classList.add('none');
    await assert_width(element, '0px');

    // none -> DocumentTimeline
    element.classList.remove('none');
    await assert_width(element, '100px');

    // DocumentTimeline -> ScrollTimeline
    element.classList.add('scroll');
    await assert_width(element, '120px');

    // ScrollTimeline -> none
    element.classList.add('none');
    await assert_width(element, '0px');

    // none -> ScrollTimeline
    element.classList.remove('none');
    await assert_width(element, '120px');
  }, 'Changing to/from animation-timeline:none');

  dynamic_rule_test(async (t, assert_width) => {
    let element = insertElement();
    insertSheet('#element { animation-timeline: timeline; }');

    await assert_width(element, '0px');

    insertScrollTimeline({source: 'selector(#scroller1)'});
    await assert_width(element, '120px');

    insertScrollTimeline({source: 'selector(#scroller2)'});
    await assert_width(element, '140px');
  }, 'Changing the source descriptor switches effective timeline');

  dynamic_rule_test(async (t, assert_width) => {
    let element = insertElement();
    insertSheet('#element { animation-timeline: timeline; }');

    await assert_width(element, '0px');

    insertScrollTimeline({timeRange: '1e10s'});
    await assert_width(element, '120px');

    insertScrollTimeline({timeRange: '1e9s'});
    await assert_width(element, '102px');
  }, 'Changing the time-range descriptor switches effective timeline');

  dynamic_rule_test(async (t, assert_width) => {
    let element = insertElement();
    insertSheet('#element { animation-timeline: timeline; }');

    await assert_width(element, '0px');

    insertScrollTimeline({start: '0px'});
    await assert_width(element, '120px');

    insertScrollTimeline({start: '50px'});
    await assert_width(element, '0px');
  }, 'Changing the start descriptor switches effective timeline');

  dynamic_rule_test(async (t, assert_width) => {
    let element = insertElement();
    insertSheet('#element { animation-timeline: timeline; }');

    await assert_width(element, '0px');

    insertScrollTimeline({end: '100px'});
    await assert_width(element, '120px');

    insertScrollTimeline({end: '10px'});
    await assert_width(element, '0px');
  }, 'Changing the end descriptor switches effective timeline');

  dynamic_rule_test(async (t, assert_width) => {
    let element = insertElement();
    let reverse = insertSheet('#element { animation-direction: reverse; }');
    insertSheet('#element { animation-timeline: timeline; }');

    await assert_width(element, '0px');

    // Note: #scroller1 is at 20%.
    insertScrollTimeline({source: 'selector(#scroller1)'});
    await assert_width(element, '180px');

    // Note: #scroller1 is at 40%.
    insertScrollTimeline({source: 'selector(#scroller2)'});
    await assert_width(element, '160px');

    reverse.remove();
    await assert_width(element, '140px');
  }, 'Reverse animation direction');

  dynamic_rule_test(async (t, assert_width) => {
    let element = insertElement();
    insertSheet('#element { animation-timeline: timeline; }');

    await assert_width(element, '0px');

    // Note: #scroller1 is at 20%.
    insertScrollTimeline({source: 'selector(#scroller1)'});
    await assert_width(element, '120px');

    let paused = insertSheet('#element { animation-play-state: paused; }');

    // We should still be at the same position after pausing.
    await assert_width(element, '120px');

    // Note: #scroller1 is at 40%.
    insertScrollTimeline({source: 'selector(#scroller2)'});

    // Even when switching timelines, we should be at the same position until
    // we unpause.
    await assert_width(element, '120px');

    // Unpausing should synchronize to the scroll position.
    paused.remove();
    await assert_width(element, '140px');
  }, 'Switching timelines while paused');

  dynamic_rule_test(async (t, assert_width) => {
    let element = insertElement();

    // Note: #scroller1 is at 20%.
    insertScrollTimeline({source: 'selector(#scroller1)'});

    await assert_width(element, '100px');

    insertSheet(`#element {
      animation-timeline: timeline;
      animation-play-state: paused;
    }`);

    // Pausing should happen before the timeline is modified. (Tentative).
    // https://github.com/w3c/csswg-drafts/issues/5653
    await assert_width(element, '100px');

    insertSheet('#element { animation-play-state: running; }');
    await assert_width(element, '120px');
  }, 'Switching timelines and pausing at the same time');
</script>
